﻿namespace CookieTerm
{
    using System;
    using System.Collections.Generic;
    using System.IO;

    public class AnsiInterpreter
    {
        /// <summary>
        /// Represents states of ANSI command parsing state machine
        /// </summary>
        enum State 
        { 
            /// <summary>
            /// Reading text
            /// </summary>
            Text, 

            /// <summary>
            /// ESC read, waiting for [
            /// </summary>
            WaitingForLBR, 

            /// <summary>
            /// Parsing ANSI command
            /// </summary>
            Escaped 
        };

        static Dictionary<char, Type> LetterToCommandMapping = new Dictionary<char, Type>
        {
            { 'A', typeof(CursorUp) },
            { 'B', typeof(CursorDown) },
            { 'C', typeof(CursorForward) },
            { 'D', typeof(CursorBack) },
            { 'E', typeof(CursorNextLine) },
            { 'F', typeof(CursorPrevLine) },
            { 'G', typeof(CursorHorizontal) },
            { 'H', typeof(CursorPosition) },
            { 'J', typeof(EraseInDisplay) },
            { 'K', typeof(EraseInLine) },
            { 'L', typeof(InsertLine) },
            //{ 'S', typeof(ScrollUp) },
            //{ 'T', typeof(ScrollDown) },
            { 'm', typeof(SelectGraphicRendition) },
            { 'n', typeof(DeviceStatusReport) },
            { 's', typeof(SaveCursorPosition) },
            { 'u', typeof(RestoreCursorPosition) }
        };
        
        private const int defaultBufferSize = 4096;
        private const int maxParameterLength = 256;

        #region State for tracking ANSI command parsing
        State state;
        #endregion

        #region Variables for tracking text reading
        byte[] textBuffer;
        int textLength;
        #endregion

        #region Variables for tracking parameter parsing
        int parameterHolder;
        int?[] parameters;
        int parameterLength;
        bool parameterOmitted;
        #endregion

        public event EventHandler<AnsiCommandReceivedEventArgs> OnAnsiCommandReceived;
        public event EventHandler<AnsiTextReceivedEventArgs> OnTextReceived;

        public int BufferSize
        {
            set
            {
                this.textBuffer = new byte[value];
            }
        }

        public bool ThrowExceptions { get; set; }

        byte[] TextBuffer
        {
            get
            {
                if (this.textBuffer == null)
                {
                    this.textBuffer = new byte[defaultBufferSize];
                }
                return this.textBuffer;
            }
        }

        public AnsiInterpreter()
        {
            this.state = State.Text;
            this.parameters = new int?[maxParameterLength];
            PrepareToReadNextParam();
        }

        public void ProcessInput(byte[] value)
        {
            for (int i = 0; i < value.Length; i++)
            {
                ProcessByte(value[i]);
            }
            FlushOutText();
        }

        void ProcessByte(byte value)
        {
            switch (this.state)
            {
                case State.Text:
                    if (value == AsciiByte.ESC)
                    {
                        this.state = State.WaitingForLBR;
                        FlushOutText();
                    }
                    else
                    {
                        TextBuffer[this.textLength++] = value;
                        if (this.textLength == defaultBufferSize)
                        {
                            FlushOutText();
                        }
                    }
                    break;
                case State.WaitingForLBR:
                    if (value == AsciiByte.LBR)
                    {
                        this.state = State.Escaped;
                    }
                    else if (ThrowExceptions)
                    {
                        throw new InvalidDataException("Invalid escape sequence, ESC not followed by [.");
                    }
                    break;
                case State.Escaped:
                    if (isLetter(value))
                    {
                        if (!this.parameterOmitted)
                        {
                            this.parameters[this.parameterLength++] = (int?)this.parameterHolder;
                        }
                        FireAnsiCommand(value);

                        PrepareToReadNextParam();
                        this.parameterLength = 0;
                        this.state = State.Text;
                    }
                    else if (value == AsciiByte.SEMICOLON)
                    {
                        this.parameters[this.parameterLength++] = this.parameterOmitted ? null : (int?) this.parameterHolder;
                        PrepareToReadNextParam();
                    }
                    else if (isDigit(value))
                    {
                        this.parameterHolder = this.parameterHolder * 10 + toDigit(value);
                        this.parameterOmitted = false;
                    }
                    else if (ThrowExceptions)
                    {
                        throw new InvalidDataException(string.Format("Invalid escape sequence: {0}", (char) value));
                    }
                    break;
            }
        }

        void FlushOutText()
        {
            if (this.textLength == 0)
            {
                return;
            }

            // If the last byte is half of a character, leave it in the buffer for next flush
            bool half = false;

            for (int i = 0; i < this.textLength; i++)
            {
                half = !half && !(isAscii(TextBuffer[i]));
            }

            if (this.textLength == 1 && half)
            {
                return;
            }

            byte[] bytes = new byte[half ? this.textLength - 1 : this.textLength];
            Array.Copy(TextBuffer, bytes, bytes.Length);

            if (half)
            {
                TextBuffer[0] = TextBuffer[this.textLength - 1];
                this.textLength = 1;
            }
            else
            {
                this.textLength = 0;
            }

            if (this.OnTextReceived != null)
            {
                this.OnTextReceived(this, new AnsiTextReceivedEventArgs(bytes));
            }
        }

        void FireAnsiCommand(byte b)
        {
            Type type;
            if (LetterToCommandMapping.TryGetValue((char)b, out type))
            {
                AnsiCommand command = (AnsiCommand) Activator.CreateInstance(type);
                command.LoadParameters(this.parameters, this.parameterLength);
                if (this.OnAnsiCommandReceived != null)
                {
                    this.OnAnsiCommandReceived(this, new AnsiCommandReceivedEventArgs(command));
                }
            }
            else if (ThrowExceptions)
            {
                throw new NotImplementedException(string.Format("Unrecognized or unsupported ANSI escape {0}", (char) b));
            }
        }

        void PrepareToReadNextParam()
        {
            this.parameterHolder = 0;
            this.parameterOmitted = true;
        }

        static bool isAscii(byte b)
        {
            return (b < 128);
        }

        static bool isLetter(byte b)
        {
            return (b <= 'z' && b >= 'a' || b <= 'Z' && b >= 'A');
        }

        static bool isDigit(byte b)
        {
            return (b <= '9' && b >= '0');
        }

        static int toDigit(byte b)
        {
            return (int)(b - '0');
        }
    }
}
